package club.jdiy.dev.controller;

import club.jdiy.admin.interceptor.GuestDisabled;
import club.jdiy.core.AppContext;
import club.jdiy.core.base.domain.Ret;
import club.jdiy.core.ex.JDiyException;
import club.jdiy.core.sql.TableInfo;
import club.jdiy.core.storage.Store;
import club.jdiy.dev.entity.JDiyUi;
import club.jdiy.dev.meta.*;
import club.jdiy.dev.service.JDiyUiService;
import club.jdiy.dev.types.*;
import club.jdiy.dev.view.DefaultFormHandler;
import club.jdiy.dev.view.FormHandler;
import club.jdiy.dev.view.FtlParser;
import club.jdiy.dev.view.Handlers;
import club.jdiy.dev.helper.OptsHelper;
import club.jdiy.core.ex.JDiyFormException;
import club.jdiy.utils.*;
import club.jdiy.core.sql.Args;
import club.jdiy.core.sql.Rs;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;

import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Controller
@RequestMapping("mgmt/JDiy")
public class JDiyInController extends JDiyUiCtrl<JDiyUi, JDiyUiService> implements JDiyController {
    @RequestMapping("{_referrerMenuId}/form.{opType}.{uid}")
    public String in(
            HttpServletResponse response,
            @PathVariable String _referrerMenuId,//来源主菜单(for权限控制/表单无用，与其它类型界面保持一致)
            @PathVariable String uid,
            @PathVariable String opType, //打开类型：dialog | tab
            String _ids,//批量修改(有id的话，以单条修改优先)
            @RequestParam Map<String, String> qo,
            ModelMap map,
            //如果来自树界面,则会带如下3个参数，以便明确树相关字段的更新(_trSkipSelf:用于树本身维护选上级时跳过自身)
            String _trPageId, String _trParentId, @RequestParam(defaultValue = "false") Boolean _trSkipSelf
    ) {
        map.put("opType", opType);
        map.put("queryParamsJson", JsonUtils.stringify(qo));

        FormUiMeta uiMeta;
        try {
            uiMeta = service.getUiMeta(uid);
        } catch (Exception ex) {
            ex.printStackTrace();
            return Ret.direct(response, Ret.Type.msg, "<div style=\"margin:10px;color:red;\">目标界面配置有误，请跟踪控制台错误日志并检查！</div>");
        }
        FtlParser parser = createFtlParser();
        parser.addVariable("qo", qo);
        preUiMeta(uiMeta, parser);

        Rs newRs = appContext.getDao().create(uiMeta.getMainTable());
        String primaryKey = newRs.getPrimaryKey();
        String _id = qo.get(primaryKey);

        boolean isAid = StringUtils.isEmpty(_id) || "0".equals(_id),//isAid表示逻辑层面的增加
                isAdd; //isAdd表示本次更新是物理上的记录添加（即该记录在库中不存在）

        Rs vo = isAid ? newRs : appContext.getDao().rs(uiMeta.getMainTable(), _id);
        vo.set(vo.getPrimaryKey(), isAid ? 0 : _id);
        isAdd = vo.isNew();
        parser.addVariable("vo", vo);


        FormHandler handler = handlers.getHandler(uiMeta.getPageHandler(), FormHandler.class).orElse(new DefaultFormHandler());

        //预处理:
        boolean hasFileObj = false, hasCascade = false;
        Map<String, Collection<OptsHelper.Entry>> voOptions = new HashMap<>();
        for (InputMeta input : uiMeta.getInputs()) {

            input.setConditionShow(
                    StringUtils.isBlank(input.getConditionShow()) || ifCondition(input, uiMeta, vo, parser)
                            ? "true"
                            : null
            );

            String cField = StringUtils.isEmpty(input.getField()) ? input.getId() : input.getField();
            if (input.getType() == InputTpl.file) hasFileObj = true;
            if (isAdd) {
                if (input.getType() == InputTpl.treeSelect && StringUtils.hasText(_trParentId)) {//树界面：添加子项 按钮处理默认选中
                    try {
                        TreeUiMeta treeUiMeta = service.getUiMeta(_trPageId);
                        if (treeUiMeta.getPidField().equals(input.getField())) vo.put(input.getField(), _trParentId);
                    } catch (Exception ignore) {
                    }
                }

                //初始值：
                if (!StringUtils.isEmpty(input.getInitial())) {
                    try {
                        vo.put(cField, parser.parse(input.getInitial()));
                    } catch (Exception ex) {
                        ex.printStackTrace();
                        vo.put(cField, input.getInitial());
                    }
                }
            }

            switch (input.getType()) {
                case lookup:
                    try {
                        if (StringUtils.isNotBlank(input.getLookupParam()))
                            input.setLookupParam(parser.parse(input.getLookupParam().trim().replaceAll("'", "")));
                        if (StringUtils.isNotBlank(input.getLookupFilter()))
                            input.setLookupFilter(Base64.getEncoder().encodeToString(parser.parse(input.getLookupFilter().trim()).getBytes()));
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                    String v = vo.getString(cField);
                    if (input.getLookupMulti() == null || !input.getLookupMulti()) {
                        if (StringUtils.hasText(v)) {
                            TableInfo lookupTableInfo = context.getDao().getTableInfo(service.getUiMeta(input.getLookupId()).getMainTable());
                            Rs rs = appContext.getDao().rs(lookupTableInfo.getTableName(),
                                    v, lookupTableInfo.getPrimaryKey() + ", " + input.getLookupField() + " as name");
                            if (!rs.isNew()) vo.put(cField + "__lookupData", new Rs[]{rs});
                        }
                    } else if (input.getLookupType() != null) {
                        if (!isAdd) {//修改表单：
                            TableInfo lookupTableInfo = context.getDao().getTableInfo(service.getUiMeta(input.getLookupId()).getMainTable());
                            switch (input.getLookupType()) {
                                case OneToMany:
                                    List<Rs> lst = appContext.getDao().ls(new Args(lookupTableInfo.getTableName(),
                                            input.getLookupJoinField() + "='" + vo.id() + "'", lookupTableInfo.getPrimaryKey() + "," + input.getLookupField() + " as name")).getItems();
                                    if (lst != null && !lst.isEmpty()) {
                                        vo.put(cField + "__lookupData", lst);
                                    }
                                    break;
                                case ManyToMany:
                                    List<Rs> mlst = appContext.getDao().ls("select r." + lookupTableInfo.getPrimaryKey() + " as id, r." + input.getLookupField() + " as name from " +
                                                    lookupTableInfo.getTableName() + " r," + input.getLookupJoinTable() + " j where j."
                                                    + input.getLookupJoinField0() + "='" + vo.id() + "' and j." + input.getLookupJoinField1() + "=r." + lookupTableInfo.getPrimaryKey(),
                                            0, 1).getItems();
                                    if (mlst != null && !mlst.isEmpty()) {
                                        vo.put(cField + "__lookupData", mlst);
                                    }
                                    break;
                            }
                        } else if (StringUtils.hasText(v)) {
                            TableInfo lookupTableInfo = context.getDao().getTableInfo(service.getUiMeta(input.getLookupId()).getMainTable());
                            //添加表单，有初始值时，根据初始值渲染，初始值可以是：  id  或  'id'  或 'id1','id2'这样的形式均可：
                            String ids = "'" + StringUtils.join(v.trim()
                                    .replaceAll("\\s", "")
                                    .replaceAll("'", "").split(","), "','") +
                                    "'";//统一转换成 'id','id','id'的形式
                            List<Rs> lst = appContext.getDao().ls(new Args(lookupTableInfo.getTableName(),
                                    lookupTableInfo.getPrimaryKey() + " in (" + ids + ")",
                                    lookupTableInfo.getPrimaryKey() + "," + input.getLookupField() + " as name")).getItems();
                            if (lst != null && !lst.isEmpty()) {
                                vo.put(cField + "__lookupData", lst);
                            }
                        }
                    }
                    break;
                case checkbox:
                    if (!isAdd && input.getManyType() != null) {
                        switch (input.getManyType()) {
                            case OneToMany:
                                if (input.getOptType() == OptTpl.table) {
                                    TableInfo optTableInfo = appContext.getDao().getTableInfo(input.getOptTable());
                                    List<Rs> lst = appContext.getDao().ls(new Args(input.getOptTable(), input.getOptValField() + "='" + vo.id() + "'", optTableInfo.getPrimaryKey())).getItems();
                                    if (lst != null && !lst.isEmpty()) {
                                        vo.put(cField, "'" + ArrayUtils.join(lst, "','", Rs::id) + "'");
                                    } else {
                                        vo.remove(cField);
                                    }
                                } else if (input.getOptType() == OptTpl.entity) {
                                    List<?> lst = em.createQuery("select o.id from " + input.getOptEntity() + " o where o."
                                                    + input.getOptValField() + "='" + vo.id() + "'")
                                            .getResultList();
                                    if (lst != null && !lst.isEmpty()) {
                                        vo.put(cField, "'" + ArrayUtils.join(lst, "','") + "'");
                                    } else {
                                        vo.remove(cField);
                                    }
                                }
                                break;
                            case ManyToMany:
                                List<Rs> lst = appContext.getDao().ls(new Args(input.getManyTable(), input.getManyField0() + "='" + vo.id() + "'", input.getManyField1() + " as refid")).getItems();
                                if (lst != null && !lst.isEmpty()) {
                                    vo.put(cField, "'" + ArrayUtils.join(lst, "','", rs -> rs.getString("refid")) + "'");
                                } else {
                                    if (cField != null) vo.remove(cField);
                                }
                                break;
                        }
                    }
                    //Don't add "break;" code here !
                case select:
                case radio:
                    voOptions.put(input.getId(), optsHelper.listOptions(input, parser));
                    if (input.isCascade()) hasCascade = true;
                    break;
                case input:
                    switch (input.getFormat()) {
                        case date:/*防止因数据库类型为datetime,到表单中文本框默认值带时间，点日期控件会报格式错*/
                            LocalDate dt = vo.getLocalDate(input.getField());
                            if(dt!=null) vo.set(input.getField(),dt);
                            break;
                        case datetime:/*bigint型的timestamp, 字符串，都能识别为datetime*/
                            LocalDateTime ldt = vo.getLocalDateTime(input.getField());
                            if(ldt!=null) vo.set(input.getField(),ldt);
                            break;
                        case joinEntity:
                            List<?> ida = em.createQuery("select o." + input.getJoinField() + " from " + input.getJoinEntity() + " o where o.id='" +
                                            vo.getString(cField) + "'")
                                    .setMaxResults(1)
                                    .getResultList();
                            if (ida != null && !ida.isEmpty()) vo.put(cField, ida.get(0));
                            break;
                        case joinTable:
                            Rs oj = appContext.getDao().rs(input.getJoinTable(), vo.getString(cField), input.getJoinField());
                            if (!oj.isNew()) vo.put(cField, oj.getString(input.getJoinField()));
                            break;
                    }
                    break;
            }
        }


        String submitUrl = context.getContextPathURL() + "/mgmt/JDiy/form.save." + uiMeta.getId();
        if (uiMeta.isAjaxSave()) {
            try {
                submitUrl = parser.parse(uiMeta.getAjaxUrl());
            } catch (Exception ignore) {
                submitUrl = uiMeta.getAjaxUrl();
            }
            String sul = submitUrl.toLowerCase();
            if (sul.startsWith("/")) {
                submitUrl = context.getContextPathURL() + submitUrl;
            } else if (!sul.startsWith("http:") && !sul.startsWith("https:")) {
                submitUrl = context.getContextPathURL() + "/mgmt/" + submitUrl;
            }

            //自定义保存时,要把隐藏域写入到表单：
            if (uiMeta.getHiddenParams() != null && !"".equals(uiMeta.getHiddenParams())) {
                String[] sa = uiMeta.getHiddenParams().split("\\n");
                List<String[]> hiddenParams = new ArrayList<>();
                for (String ss : sa) {
                    String[] sb = ss.split(":");
                    if (sb.length == 2) {
                        String v = sb[1].trim(), f = sb[0].trim();
                        boolean force = f.startsWith("@");
                        try {
                            String hv = force || vo.isNew() || !vo.containsKey(f) ? parser.parse(v) : vo.getString(f);
                            if (hv != null)
                                hiddenParams.add(new String[]{force ? f.substring(1).trim() : f, hv});
                        } catch (Exception ex) {
                            log.error("隐藏域配置错误,请检查." + f);
                            ex.printStackTrace();
                        }
                    }
                }
                map.put("hiddenParams", hiddenParams);
            }
        }
        map.put("submitUrl", submitUrl);

        if (hasFileObj) map.put("store", appContext.getStore(uiMeta.getMainTable(), vo.id()));
        handler.onView(vo, qo);
        map.put("vo", vo);
        map.put("primaryKey", vo.getPrimaryKey());
        map.put("_id", _id);
        map.put("hasCascade", hasCascade);
        map.put("isAdd", isAdd);
        map.put("voOptions", voOptions);
        map.put("uiMeta", uiMeta);
        map.put("hasFileObj", hasFileObj);
        map.put("_trPageId", _trPageId);
        map.put("_trParentId", _trParentId);
        map.put("_trSkipSelf", _trSkipSelf);
        map.put("_ids", _ids);
        return "form.render";
    }

    @ResponseBody
    @RequestMapping("form.cascade.{uid}")
    public Ret<?> cascadeOptions(@PathVariable String uid, String dataId, String name, String value) {
        try {
            FormUiMeta uiMeta;
            try {
                uiMeta = service.getUiMeta(uid);
            } catch (Exception ex) {
                return Ret.error(ex);
            }

            Rs rs = context.getDao().rs(uiMeta.getMainTable(), dataId);
            rs.set(name, value);

            FtlParser parser = createFtlParser();
            parser.addVariable("vo", rs);

            Map<String, Collection<OptsHelper.Entry>> voOptions = Arrays.stream(uiMeta.getInputs())
                    .filter(input ->
                            input.isCascade()
                                    && (input.getType() == InputTpl.select || input.getType() == InputTpl.radio || input.getType() == InputTpl.checkbox)
                                    && JsonUtils.stringify(input).contains("${vo." + name)
                    )
                    .collect(Collectors.toMap(InputMeta::getField, input -> optsHelper.listOptions(input, parser), (a, b) -> b));
            return Ret.success(voOptions);
        } catch (Exception e) {
            e.printStackTrace();
            return Ret.error(e);
        }
    }

    @GuestDisabled
    @RequestMapping("form.save.{uid}")
    @ResponseBody
    public Ret<?> save(@PathVariable String uid, @RequestParam Map<String, String> qo,
                       HttpServletRequest request) {
        try {
            FormUiMeta uiMeta;
            try {
                uiMeta = service.getUiMeta(uid);
            } catch (Exception ex) {
                return Ret.error(ex);
            }

            Map<String, Object> __formQueryParams;//获取表单界面url中的query参数(注意不是表单里的)
            try {
                __formQueryParams = JsonUtils.parse(qo.get("__formQueryParams"), HashMap.class);
            } catch (Exception ignore) {
                __formQueryParams = new HashMap<>();
            }
            __formQueryParams.putAll(qo);

            //将多选框钩选项变成 'A','B' 这样的格式/顺便检查一下钩选
            Arrays.stream(uiMeta.getInputs()).filter(it -> it.getType() == InputTpl.checkbox).forEach(it -> {
                String cField = StringUtils.isEmpty(it.getField()) ? it.getId() : it.getField();
                String[] vs = request.getParameterValues(cField);
                int valLen = vs == null ? 0 : vs.length;
                if (it.getMinChk() != null && it.getMinChk() > valLen)
                    throw new JDiyFormException(it.getLabel() + " 最少需钩选" + it.getMinChk() + "项！", it.getField());
                if (it.getMaxChk() != null && it.getMaxChk() < valLen)
                    throw new JDiyFormException(it.getLabel() + " 最多只能钩选" + it.getMaxChk() + "项！", it.getField());
                if (valLen > 0) qo.put(cField, "'" + ArrayUtils.join(vs, "','") + "'");
            });

            FormHandler handler = handlers.getHandler(uiMeta.getPageHandler(), FormHandler.class).orElse(new DefaultFormHandler());
            FtlParser parser = createFtlParser();
            parser.addVariable("qo", __formQueryParams);


            //=======================================================================================列表页顶部的选中项弹窗批量更新表单保存：
            String pk = context.getDao().create(uiMeta.getMainTable()).getPrimaryKey();
            String dataId = qo.get(pk);
            boolean isBatchUpdate = (!StringUtils.hasText(dataId) || "0".equals(dataId)) && StringUtils.hasText(qo.get("_ids"));
            if (isBatchUpdate) {
                service.do_batchUpdate(qo, uiMeta, handler, parser);
                return Ret.success();
            }
            //=======================================================================================


            Rs rs = service.do_save(dataId, qo, uiMeta, handler, parser);
            try {
                if (MultipartRequest.class.isAssignableFrom(request.getClass())) {
                    MultipartRequest mRequest = (MultipartRequest) request;
                    Store store = appContext.getStore(rs.getTable(), rs.id());
                    List<MultipartFile> files;
                    String s, allowExt;
                    for (InputMeta input : uiMeta.getInputs()) {
                        if (input.getType() == InputTpl.file && (files = mRequest.getFiles(input.getField())).size() > 0) {
                            for (MultipartFile mf : files) {
                                if (mf.isEmpty()) continue;
                                if (input.getFileSize() != null && input.getFileSize() > 0 && mf.getSize() / 1000 > input.getFileSize())
                                    continue;//客户端判断了文件大小，这儿只是简单忽略(linux显示的是1000进一个单位)
                                s = mf.getContentType();
                                if (s != null && !"*/*".equals(input.getFileType().getMime()) && !input.getFileType().getMime().contains(s))
                                    throw new JDiyFormException("不允许的上传文件MIME类型:" + s + "；上传文件：" + mf.getOriginalFilename(), input.getField());
                                allowExt = input.getFileType() == FileTypeTpl.custom ? input.getFileExtensions() : null;
                                final String fn = mf.getOriginalFilename() + "";
                                String ext = fn.substring(fn.lastIndexOf(".") + 1);
                                if (allowExt != null) {
                                    if (!allowExt.contains(ext))
                                        throw new JDiyFormException("不允许的上传文件类型．需要：" + allowExt, input.getField());
                                }
                                File nf = new File("/tmp/" + IdWorker.getId() + "__" + fn);
                                FileUtils.write(nf, mf.getInputStream());
                                processPicture(nf, input);

                                String fileName = null;
                                if (input.getRenameType() == 1) fileName = fn;//保持原名
                                else if (input.getRenameType() == 2) {
                                    if ("one".equals(input.getFileMultipart())) {
                                        fileName = StringUtils.isBlank(input.getFileName())
                                                ? input.getField()
                                                : parser.parse(input.getFileName());
                                        if (!fileName.toLowerCase().endsWith("." + ext)) ;
                                    } else
                                        fileName = StringUtils.isBlank(input.getFileName()) ? input.getField() : parser.parse(input.getFileName());
                                    if (fileName.toLowerCase().endsWith("." + ext))
                                        fileName = fileName.substring(0, fileName.length() - ext.length() - 1);
                                    fileName = getSerialFileName(store.get(input.getField()).getUrls(), fileName, ext);
                                }
                                if (Boolean.TRUE.equals(input.getNoStore())) {

                                    String aUrl = store.put(nf, fileName);
                                    if (StringUtils.hasText(aUrl)) {
                                        if ("one".equals(input.getFileMultipart())) rs.set(input.getField(), aUrl);
                                        else {
                                            List<String> ls = new ArrayList<>();
                                            if (rs.get(input.getField()) != null) {
                                                String[] urls = rs.getString(input.getField()).split("\\|");
                                                for (String u : urls) {
                                                    if (StringUtils.hasText(u)) ls.add(u);
                                                }
                                            }
                                            ls.add(aUrl);
                                            rs.set(input.getField(), ArrayUtils.join(ls, "|"));
                                        }
                                    }
                                } else {
                                    if ("one".equals(input.getFileMultipart()))
                                        store.set(input.getField(), nf, fileName);
                                    else store.add(input.getField(), nf, fileName);
                                    rs.set(input.getField(), ArrayUtils.join(store.get(input.getField()).getUrls(), "|"));
                                }
                                //noinspection ResultOfMethodCallIgnored
                                nf.delete();
                            }
                        }
                    }
                    appContext.getDao().save(rs);
                }
            } catch (Exception ioe) {
                ioe.printStackTrace();
                handler.completeSave(rs);
                return Ret.error(ioe);
            }

            if (StringUtils.hasText(qo.get("_trPageId"))) {//树界面要根据pidField更新界面，这儿返回为 jdiyTreeParentId。
                Map<String, String> ret = new HashMap<>();
                try {
                    TreeUiMeta tm = service.getUiMeta(qo.get("_trPageId"));
                    ret.put("jdiyTreeParentId", rs.getString(tm.getPidField()));
                } catch (Exception ignore) {
                }
                handler.completeSave(rs);
                return Ret.success(ret);
            } else {
                handler.completeSave(rs);
                return Ret.success(rs);
            }

        } catch (Exception e) {
            return Ret.error(e);
        }
    }

    private void processPicture(File file, InputMeta it) throws IOException {
        if (it.getFileType() != FileTypeTpl.image || RoomTpl.none == it.getRoom()) return;
        PicUtils pic = new PicUtils(file);
        int oldW = pic.getWidth(), oldH = pic.getHeight();
        if (oldW == it.getRoomW() && oldH == it.getRoomH()) return;
        switch (it.getRoom()) {
            case only:
                pic.resizeBy(it.getRoomW(), it.getRoomH());
                break;
            case white:
                pic.resizeBy(it.getRoomW(), it.getRoomH(), Color.WHITE);
                break;
            case opacity:
                pic.resizeBy(it.getRoomW(), it.getRoomH(), null);
                if (!file.getName().toLowerCase().endsWith(".png")) {
                    String newFn = file.getAbsoluteFile().toString();
                    newFn = newFn.substring(0, newFn.lastIndexOf(".")) + ".png";
                    //noinspection ResultOfMethodCallIgnored
                    file.delete();
                    pic.saveAs(new File(newFn));
                    return;
                }
                break;
            case force:
                pic.resizeTo(it.getRoomW(), it.getRoomH());
                break;
        }
        pic.save();
    }

    private String getSerialFileName(String[] urls, String fileName, String fileExt) {
        boolean notOk = false;
        int i = 1;
        for (String u : urls) {
            if (u.endsWith("/" + fileName + "." + fileExt)) {
                notOk = true;
                break;
            }
        }
        if (notOk)
            while (true) {
                boolean ok = true;
                String nf=fileName + "_" + i++;
                for (String u : urls) {
                    if (u.endsWith("/" + nf + "." + fileExt)) {
                        ok = false;
                        break;
                    }
                }
                if (ok){
                    fileName=nf;
                    break;
                }
            }
        return fileName+"."+fileExt;
    }


    static boolean ifCondition(InputMeta im, UiMeta uiMeta, Rs rs, FtlParser parser) {
        try {
            return parser.condition(im.getConditionShow());
        } catch (Exception ex) {
            log.error("\r\n表单输入控件的[显示条件]配置有误，执行条件抛出异常，请检查界面配置。\r\n界面ID:" + uiMeta.getId() + "　表单输入控件ID:" + im.getId() +
                    "　控件标题:" + im.getLabel() + "\r\n出错的输入控件显示条件配置内容：" + im.getConditionShow() +
                    (rs == null ? "" : "\r\n业务数据:" + uiMeta.getMainTable() + "(" + rs.getPrimaryKey() + "=" + rs.id() + ")"));
            ex.printStackTrace();
            return false;
        }
    }

    @PersistenceContext
    private EntityManager em;
    @Resource
    private OptsHelper optsHelper;
    @Resource
    private AppContext appContext;
    @Resource
    private Handlers handlers;
}
